/**
 * 
 */
package org.dragonfire.dasm;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Scanner;

import org.dragonfire.config.Config;
import org.dragonfire.config.INIConfig;
import org.dragonfire.config.ResourceConfig;
import org.dragonfire.dasm.functions.File;
import org.dragonfire.dasm.functions.Line;
import org.dragonfire.dasm.functions.Module;
import org.dragonfire.dasm.functions.PC;
import org.dragonfire.dasm.functions.Param;
import org.dragonfire.dasm.instr.InstructionTable;
import org.dragonfire.dasm.linkers.Linker;
import org.dragonfire.dasm.postprocessor.Postprocessor;
import org.dragonfire.dasm.preprocessor.Block;
import org.dragonfire.dasm.preprocessor.Preprocessor;
import org.dragonfire.file.BufferedFile;
import org.dragonfire.parser.Equation;
import org.dragonfire.parser.functions.Function;
import org.dragonfire.util.DMath;
import org.dragonfire.util.DString;
import org.dragonfire.util.TimeProfile;

import com.stevesoft.pat.RegRes;
import com.stevesoft.pat.Regex;
import com.stevesoft.pat.ReplaceRule;
import com.stevesoft.pat.StringBufferLike;

/**
 * @author dandroid
 * @date Dec 7, 2006
 */
public class DASM {

	public static final ResourceConfig LANG = new ResourceConfig("org.dragonfire.dasm.lang.dasm");
	public static final String WORD_REGEX = "\\b[A-z_][A-z0-9_\\.]*\\b";
	/**
	 * A regular expression pattern that matches labels in the format
	 * name@module
	 */
	public static final String LABEL_REGEX = WORD_REGEX + "(\\@" + WORD_REGEX + ")?";
	public static final String COMMAND_REGEX = "^([\\.\\#]?" + WORD_REGEX + ")(?:(?:\\s+(.*)?)|(?:\\s*\\((.*)\\)))?$";
	public static final String LITERAL_REGEX = "[A-z0-9\\(\\)\\.\\_\\-\\']+";
	/**
	 * A regular expression pattern that matches a register
	 */
	public static final String REGISTER_REGEX = "[A-z]\\w*";
	/**
	 * A regular expression pattern that matches index-offset parameters in the
	 * format (index+offset)
	 */
	public static final String OFFSET_REGEX = "\\(" + REGISTER_REGEX + "\\+.*\\)";
	/**
	 * The default prefile to process before the specified source file
	 */
	public static final String DEFAULT_PRE_FILE = "<pre.inc>";
	/**
	 * The default postfile to process after the specified source file
	 */
	public static final String DEFAULT_POST_FILE = "<post.inc>";
	public static final String DEFAULT_HEADER_FILE = "headers.inc";
	/**
	 * DASM's version, both major and minor
	 */
	public static final double VERSION = 2.0;
	/**
	 * Additional version status, such as "alpha" or "release canadate"
	 */
	public static final String VERSION_APPEND = " Alpha 5";
	/**
	 * The character that denotes a comment
	 */
	public static final char COMMENT_CHAR = ';';
	/**
	 * The character the denotes a newline between instructions and macros
	 */
	public static final char NEWLINE_CHAR = '\\';
	/**
	 * The character that separates parameters passed to directives,
	 * instructions, and macros
	 */
	public static final char PARAM_SEP_CHAR = ',';
	/**
	 * The author's name and email address
	 */
	public static final String AUTHOR = "Darth Android (darthandroid@gmail.com)";
	/**
	 * The name of the section in the <code>CONFIG</code>,
	 * <code>CONFIG.txt</code>, or <code>CONFIG.ini</code> file which
	 * contains default parameters
	 */
	public static final String PARAM_SECTION = "params";

	public int getPass() {
		return pass;
	}

	static {
		// TODO check if this is still used anywhere
		Regex.define("word", WORD_REGEX);
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		/*
		 * args = new String[] { "--no-wait", "--no-postfile", "<tests/test.asm>",
		 * "--debug", "--compat-tasm", "--listfile=<STDOUT>", "--blah=workms" };
		 */
		// initialize the compiler
		DASM d = new DASM();
		// prompt for args if none are provided
		if (args.length == 0) {
			// hold the process running since no arguments likely means the
			// program was launched from a double-click and the console is only
			// open until the process terminates
			d.setParam("wait-keypress", "true");
			// prompt for arguments
			System.out.println("Enter arguments, and then press return:");
			Scanner in = new Scanner(System.in);
			String newArgs = in.nextLine();
			// process string into args
			args = DString.splitTopLevel(newArgs, ' ');
			// strip quotes used to enclose spaces
			for (int i = 0; i < args.length; i++) {
				if (args[i].matches("\\\".*\\\"")) {
					args[i] = args[i].substring(1, args[i].length() - 1);
				}
			}
		}
		// process arguments
		d.parseArgs(args);
		// build the file
		int times = 1;
		for (int i = 0; i < times; i++)
			d.compile();
		// wait and keep the program running. This is designed for when the
		// executable is double-clicked and the window only stays open until the
		// program terminates. We need to keep the window open so that the user
		// can see the output
		if (d.getBooleanParam("wait-keypress"))
			waitForKeypress();
		// exit with the number of errors encountered
		System.exit(d.getNumErrors());
	}

	public class VarReplaceRule extends ReplaceRule {

		public void apply(StringBufferLike sb, RegRes rr) {
			String var = rr.stringMatched();
			String value = getDefine(var);
			if (value == null) {
				if (pass == 2)
					throw new CompileException("ERROR_EQUATE_NOT_DEFINED", var);
				else
					sb.append("0");
			}
			else
				sb.append(value);
		}
	}

	private Equation equ = new Equation();
	private Preprocessor prep;
	private int pass;
	private Postprocessor post;
	private ArrayList<Listing> listing = new ArrayList<Listing>();
	private InstructionTable table;
	private HashMap<String, String> defines;
	private byte align;
	private int warnings = 0;
	private int errors = 0;
	private boolean onNewLine = true;
	private Config params = new INIConfig();
	private PrintStream logFile = System.out;
	private DirectiveManager dm = new DirectiveManager(this);

	/**
	 * Returns the {@link org.dragonfire.parser.Equation} object initialized for
	 * use with this compiler.
	 * 
	 * @return An {@link org.dragonfire.parser.Equation} that has been properly
	 *         initialized.
	 */
	public Equation getParser() {
		return equ;
	}

	private void fillInParams() {
		// everything begins with the input, if we don't have one, exit out
		if (getParam("sourcefile") == null || getParam("sourcefile").equals(""))
			return;
		// determine input format
		if (getParam("source-format") == null) {
			// not much here yet, DASM only works with assembly code so far
			setParam("source-format", "asm");
		}
		// determine output file
		if (getParam("outputfile") == null) {
			String sourcefile = getParam("sourcefile");
			sourcefile = sourcefile.replaceAll(".*[\\\\\\/]", "").replaceAll("\\..*", "").replaceAll(
					"[\\\\\\/\\:\\*\\?\\\"\\<\\>\\|]", "");
			// if both output file and output format are undefined, output
			// format = 8xp
			if (getParam("output-format") == null) {
				setParam("output-format", "8xp");
			}
			setParam("outputfile", sourcefile + "." + getParam("output-format"));
		}
		// find output format
		if (getParam("output-format") == null || getParam("output-format").equals("")) {
		}
	}

	public void compile() {
		// in order to allow compile() to be called multiple times, we need to
		// save the initial configuration
		Config paramBackup = getConfig();
		// fill in missing params
		fillInParams();
		// open log file
		if (getParam("logfile") != null) {
			logFile = BufferedFile.openOutputFile(getParam("logfile"));
		}
		try {
			// build the file
			List<Byte> data = build(getParam("sourcefile"));
			// link the data
			data = Linker.link(this, data, getParam("output-format"));
			// write the file
			PrintStream ps = BufferedFile.openOutputFile(getParam("outputfile"));
			for (Byte b : data) {
				ps.write(b);
			}
		} finally {
			// replace the original config even if errors
			setConfig(paramBackup);
		}
	}

	public DirectiveManager getDM() {
		return dm;
	}

	public String getCurrentModule() {
		String[] stack = getCurrentModuleStack();
		if (stack.length == 0)
			return "";
		return stack[stack.length - 1];
	}

	public String[] getCurrentModuleStack() {
		switch (pass) {
			case 1:
				return prep.getCurrentModuleStack();
			case 2:
				return post.getCurrentModuleStack();
			default:
				return new String[0];
		}
	}

	/**
	 * Constructs an initializes a <code>DASM</code> object. This loads DASM's
	 * custom functions and reads in any configuration files that can be found
	 * in the working directory
	 * 
	 * @see #reset()
	 * @see #DASM(String[])
	 */
	public DASM() {
		// reset
		reset();
		// load DASM-specific functions
		equ.putFunction("pc", new PC(this));
		equ.putFunction("line", new Line(this));
		equ.putFunction("file", new File(this));
		equ.putFunction("module", new Module(this));
		equ.putFunction("param", new Param(this));
		// set the replace rule for variables
		equ.setVarRule(new VarReplaceRule());
		// create an alias from $ to $pc
		Function.createGenericFunction("", "$pc", 0);
	}

	/**
	 * Constructs an initializes a <code>DASM</code> object, and then reads in
	 * an array of arguments. During initialization, configuration files are
	 * read in and then the arguments supplied to the method are processed. This
	 * allows the given arguments to override any found in the configuration
	 * files.
	 * 
	 * @see #DASM()
	 * @see #parseArgs(String[])
	 * @param args
	 *            Initial arguments to the compiler
	 */
	public DASM(String[] args) {
		// initialize
		this();
		// process arguments
		parseArgs(args);
	}

	/**
	 * Resets and reinitializes the compiler. This allows multiple files to be
	 * built with the same compiler object.
	 */
	public void reset() {
		// dump objects and start anew
		params = new INIConfig();
		prep = new Preprocessor(this);
		pass = 0;
		post = new Postprocessor(this);
		listing = new ArrayList<Listing>();
		table = new InstructionTable(this);
		defines = new HashMap<String, String>();
		// define initial defaults
		setParam("prefile", DEFAULT_PRE_FILE);
		setParam("postfile", DEFAULT_POST_FILE);
		align = -1;
		// import CONFIG, CONFIG.txt, and CONFIG.ini files from the working
		// directory and load parameters from them
		importExternalConfig(params);
	}

	/**
	 * Returns the line number of the current line that is being processed, or
	 * -1 if no line is being processed.
	 * 
	 * @return The line number of the current line being processed.
	 */
	public int getCurrentLineNum() {
		switch (pass) {
			case 1:
				return prep.getCurrentLineNum();
			case 2:
				return post.getCurrentLineNum();
			default:
				return -1;
		}
	}

	public String getCurrentFileName() {
		switch (pass) {
			case 1:
				return prep.getCurrentFileName();
			case 2:
				return post.getCurrentFileName();
			default:
				return "<Unknown>";
		}
	}

	public void parseArgs(String[] args) {
		for (int i = 0; i < args.length; i++) {
			String arg = args[i];
			if (arg.length() < 2 || !arg.substring(0, 2).equals("--")) {
				if (getParam("sourcefile") == null)
					setParam("sourcefile", arg);
				else if (getParam("outputfile") == null)
					setParam("outputfile", arg);
				else
					System.out.println(LANG.getString("ERROR_UNKNOWN_ARG", arg));
			}
			else if (arg.equalsIgnoreCase("--debug"))
				setParam("debug", true);
			else if (arg.equalsIgnoreCase("--no-prefile"))
				setParam("prefile", null);
			else if (arg.equalsIgnoreCase("--no-postfile"))
				setParam("postfile", null);
			else {
				// strip beginning
				arg = arg.substring(2);
				if (arg.equals(""))
					continue;
				// process the param
				String[] parts = arg.split("=", 2);
				String key = parts[0];
				String value = parts.length > 1 ? parts[1] : "";
				// check if the beginning is "no-" in cases such as "no-wait" or
				// "no-prefile" and undefine them
				if (key.length() > 2 && key.substring(0, 3).equalsIgnoreCase("no-") && value.equals("")) {
					value = "false";
					key = key.substring(3);
				}
				// if there is no value, then default to "true" so that --debug
				// will appear as debug=true
				if (value != null && value.equals(""))
					value = "true";
				// insert the key/value
				setParam(key, value);
			}
		}
	}

	public void setAlign(byte align) {
		this.align = align;
	}

	public byte getAlign() {
		return align;
	}

	public void setAlign(int align) {
		setAlign((byte) align);
	}

	public boolean isLegacyDefine() {
		return getBooleanParam("legacy-define");
	}

	public boolean isLegacyEqu() {
		return getBooleanParam("legacy-equ");
	}

	public String eval(String equation) {
		return equ.eval(equation);
	}

	public String evalStatics(String equation) {
		if (equation == null)
			equation = "";
		equation = Equation.cleanSpaces(equation);
		equation = Equation.evalLiterals(equation);
		Equation.mismatchCheck(equation);
		equation = equ.parseFunctions(equation);
		return equation;
	}

	public ArrayList<Byte> build(String source) {
		// print program log headers
		logln("DASM " + LANG.getString("VERSION", VERSION, VERSION_APPEND));
		logln(LANG.getString("WRITTEN_BY", AUTHOR));
		logln(LANG.getString("LANG_CREDIT"));
		logln();
		logln(LANG.getString("STATS_PASS1_START"));
		logln();
		try {
			// pass 1
			pass = 1;
			// time the pass
			long pass1Start = System.nanoTime();
			// process the prefile if one is set
			if (getParam("prefile") != null && !getParam("prefile").equals(""))
				listing.addAll(prep.preprocessFile(BufferedFile.openBufferedFile(getParam("prefile"))));
			// process the file
			listing.addAll(prep.preprocessFile(BufferedFile.openBufferedFile(source)));
			// process the postfile if one is set
			if (getParam("postfile") != null && !getParam("postfile").equals(""))
				listing.addAll(prep.preprocessFile(BufferedFile.openBufferedFile(getParam("postfile"))));
			// save the time of the first pass
			long pass1Time = System.nanoTime() - pass1Start;
			logln();
			{
				if (prep.getBlockDepth() > 0) {
					warn("WARN_REMAINING_BLOCKS", prep.getBlockDepth());
					while (prep.getBlockDepth() > 0) {
						Block b = prep.getCurrentBlock();
						prep.popBlock();
						warnRaw(DASM.LANG.getString("WARNING", b.getFile(), b.getLine(), b.getName()));
					}
				}
			}
			logln(LANG.getString("STATS_PASS1_COMPLETE", pass1Time / 1000000000.0));
			// switch to pass 2.
			logln(LANG.getString("STATS_PASS2_START"));
			logln();
			pass = 2;
			long pass2Start = System.nanoTime();
			post.process(listing);
			long pass2Time = System.nanoTime() - pass2Start;
			logln();
			logln(LANG.getString("STATS_PASS2_COMPLETE", pass2Time / 1000000000.0));
			logln(LANG.getString("STATS_BUILD_TIME", source, (pass2Time + pass1Time) / 1000000000.0, prep
					.getAbsLineNum(), prep.getAbsLineNum() / ((pass2Time + pass1Time) / 1000000000.0)));
		} catch (CompileException ce) {
			error(ce);
		} catch (Throwable e) {
			StackTraceElement[] trace = e.getStackTrace();
			error(trace[0].getFileName(), trace[0].getLineNumber(), LANG.getString("ERROR_INTERNAL", AUTHOR));
			if (getBooleanParam("debug")) {
				e.printStackTrace(System.err);
			}
		}
		logln(LANG.getString("STATS_PROBLEMS", getNumErrors(), getNumWarnings()));
		if (getNumWarnings() > 0) {
			logln(LANG.getString("WARNING_COMPILE_WARNINGS"));
		}
		if (getNumErrors() > 0) {
			logln(LANG.getString("ERROR_COMPILE_ERRORS"));
			System.exit(1);
		}
		if (getBooleanParam("debug")) {
			logln("\n" + LANG.getString("DEBUG_HEADER"));
			logln(LANG.getString("DEBUG_LINE", LANG.getString("DEBUG_EQUATES", defines.size()), LANG.getString(
					"DEBUG_INSTRUCTIONS", getInstructions().getCommandCount()))
					+ "\n");
			// TODO EXTERNALIZE STRINGS
			logln("Profiling:");
			TimeProfile[] profiles = TimeProfile.getProfiles();
			Arrays.sort(profiles);
			for (TimeProfile tp : profiles) {
				logln(String.format("%-20s %.4f seconds (%8.3f avg time (msec), %10.3f counts/sec)", tp.getName(), tp
						.getTime() / 1000000000.0, tp.getAvgTime() / 1000000.0, (double) tp.getCount() / tp.getTime()
						* 1000000000));
			}
			logln();
		}
		ArrayList<Byte> output = new ArrayList<Byte>();
		for (Listing l : listing) {
			for (byte b : l.getMCode()) {
				output.add(b);
			}
		}
		pass = 0;
		if (getParam("listfile") != null && !getParam("listfile").equals("")) {
			PrintStream ps = BufferedFile.openOutputFile(getParam("listfile"));
			ps.println("      Filename       Line Address  Code");
			for (Listing l : listing) {
				if (l.isVisible())
					ps.println(l);
			}
		}
		return output;
	}

	public int getNumErrors() {
		return errors;
	}

	public int getNumWarnings() {
		return warnings;
	}

	public void error(CompileException ce) {
		error(ce.getFile(), ce.getLine(), ce.getMessage());
	}

	public void error(String file, int line, String text) {
		errors++;
		logln(LANG.getString("ERROR", file, line, text));
	}

	public void error(String errorMsgKey, Object... params) {
		error(getCurrentFileName(), getCurrentLineNum(), LANG.getString(errorMsgKey, params));
	}

	public void logln(String text) {
		log((onNewLine ? "" : ((char) 13) + "\n") + text + ((char) 13) + "\n");
		onNewLine = true;
	}

	public void logln() {
		logln("");
	}

	public void log(String text) {
		if (text.length() == 0)
			return;
		if (text.charAt(text.length() - 1) == '\n')
			onNewLine = true;
		else
			onNewLine = false;
		// TODO file logging
		logFile.print(text);
	}

	public void prepCompileException(CompileException ce) {
		ce.setFile(getCurrentFileName());
		ce.setLine(getCurrentLineNum());
	}

	public void define(String key, String module, String value) {
		if (key.matches(WORD_REGEX) && (module.length() == 0 || module.matches(WORD_REGEX)))
			if (getBooleanParam("case-sensitive"))
				defines.put(key + "@" + module, value);
			else
				defines.put(key.toLowerCase() + "@" + module.toLowerCase(), value);
	}

	public void undefine(String key, String module) {
		define(key, module, null);
	}

	public String getDefine(String name, String module) {
		if (getBooleanParam("case-sensitive"))
			return defines.get(name + "@" + module);
		return defines.get(name.toLowerCase() + "@" + module.toLowerCase());
	}

	public boolean isDefined(String name) {
		return null != getDefine(name);
	}

	public String getDefine(String name) {
		String module = getCurrentModule();
		if (name.contains("@")) {
			module = name.split("@", 2)[1];
			name = name.split("@", 2)[0];
		}
		else {
			String[] modules = getCurrentModuleStack();
			for (int i = modules.length - 1; i >= 0; i--) {
				if (defines.containsKey(name + "@" + modules[i])) {
					module = modules[i];
					break;
				}
			}
		}
		return getDefine(name, module);
	}

	/**
	 * Alias to {@link org.dragonfire.parser.Equation#evalBoolean(String)}
	 * 
	 * @param equation
	 *            The equation to parse and convert to boolean
	 * @return
	 */
	public boolean evalBool(String equation) {
		return equ.evalBoolean(equation);
	}

	public String evalString(String equation) {
		String equa = eval(equation);
		try {
			return DString.extractString(equa);
		} catch (Exception e) {
			throw new CompileException("ERROR_STRING_REQUIRED", equation);
		}
	}

	public int evalInt(String equation) {
		String equ = eval(equation);
		try {
			return (int) DMath.parseDouble(equ);
		} catch (NumberFormatException ne) {
			throw new CompileException("ERROR_INT_REQUIRED", equation);
		}
	}

	public double evalDouble(String equation) {
		String equ = eval(equation);
		try {
			return DMath.parseDouble(equ);
		} catch (NumberFormatException ne) {
			throw new CompileException("ERROR_NUM_REQUIRED", equation);
		}
	}

	public static void waitForKeypress() {
		try {
			System.out.println(LANG.getString("WAIT_KEYPRESS"));
			System.in.read();
		} catch (Exception e) {
		}
	}

	public boolean isDebugging() {
		return getBooleanParam("debug");
	}

	public String getParam(String key) {
		return getConfig().getValue(PARAM_SECTION, key);
	}

	public boolean getBooleanParam(String key) {
		return getConfig().getBoolean(PARAM_SECTION, key);
	}

	public void setParam(String key, String value) {
		getConfig().putValue(value, PARAM_SECTION, key);
	}

	public void setParam(String key, Object value) {
		getConfig().putValue(value == null ? null : value.toString(), PARAM_SECTION, key);
	}

	public Config getConfig() {
		return params;
	}

	public void setConfig(Config newConfig) {
		params = newConfig;
	}

	public String prepLine(String s) {
		s = DString.collapseSpaces(s);
		s = DString.trim(s);
		String[] parts = DString.splitTopLevel(s, COMMENT_CHAR);
		if (parts.length == 0)
			s = "";
		else
			s = parts[0];
		if (s.length() > 0 && s.charAt(0) == ';')
			s = "";
		if (getBooleanParam("compat-tasm")) {
			Regex equ = new Regex("(?Q)\\s*((??word))\\s+\\.equ\\s+(.*)", "$1 = $2");
			s = equ.replaceFirst(s);
		}
		return s;
	}

	public void warnMsg(String message) {
		warnRaw(LANG.getString("WARNING", getCurrentFileName(), getCurrentLineNum(), message));
	}

	public void warnRaw(String text) {
		warnings++;
		logln(text);
	}

	public void warn(String message, Object... params) {
		warnMsg(LANG.getString(message, params));
	}

	public static void importExternalConfig(Config conf) {
		// import CONFIG and CONFIG.txt and CONFIG.ini files as params
		try {
			FileInputStream fis = new FileInputStream("CONFIG.ini");
			conf.importFile(fis);
		} catch (FileNotFoundException fe) {
			// ignore
		}
		try {
			FileInputStream fis = new FileInputStream("CONFIG");
			conf.importFile(fis);
		} catch (FileNotFoundException fe) {
			// ignore
		}
		try {
			FileInputStream fis = new FileInputStream("CONFIG.txt");
			conf.importFile(fis);
		} catch (FileNotFoundException fe) {
			// ignore
		}
	}

	public Preprocessor getPreprocessor() {
		// only return the preprocessor while it's valid.
		return pass == 1 ? prep : null;
	}

	public Postprocessor getPostprocessor() {
		return post;
	}

	public InstructionTable getInstructions() {
		return table;
	}
}
